package baseFx.database.monitor;

import baseFx.common.Assert;
import baseFx.common.context.TypeResolver;
import baseFx.common.concurrent.Task;
import baseFx.database.TransFinishArgs;
import baseFx.database.Transaction;
import baseFx.database.execute.*;

import java.util.*;

public class DbExecuteMonitor {
    private TypeResolver rootResolver;
    private WatchSetting setting = new WatchSetting();
    private static final String beforeKey = UUID.randomUUID().toString();
    private static final String afterKey = UUID.randomUUID().toString();
    private static final String trankey = UUID.randomUUID().toString();
    private static final String tranbeforeEIDKey = UUID.randomUUID().toString();
    private static final String tranafterEIDKey = UUID.randomUUID().toString();
    private static final String watching = UUID.randomUUID().toString();
    private List<WatchContext> afterContexts = new ArrayList<>();
    private int afterDelay = 5000;
    private Boolean closed = false;

    public DbExecuteMonitor(TypeResolver resolver) {
        this.rootResolver = resolver;
        executeAfterContext();
    }

    public void close() {
        if (closed == false) {
            setting.clear();
            setting = null;
            closed = true;
        }
    }

    public void loadWatcher(Class<?>... watcher) {
        for (Class<?> item : watcher) {
            if (item.isAnnotationPresent(DbWatcher.class)) {
                setting.load(item);
            }
        }
    }

    public void attach(DbExecute execute) {
        Assert.isFalse(closed, "监视器已关闭");
        execute.eventBefore().add(this::executeBefore);
        execute.eventAfter().add(this::executeAfter);
    }

    private void executeBefore(Object sender, DbExecuteBeforeArgs args) {
        DbExecute execute = (DbExecute) sender;
        Map<String, Object> userdata = execute.userdata();
        if (setting.exists(true, args.getTable(), convert(args.getOperation()))) {
            WatchContext ctx = new WatchContext();
            ctx.model = args.getModel();
            ctx.operation = convert(args.getOperation());
            ctx.table = args.getTable();
            ctx.isBefore = true;

            if (userdata.containsKey(watching)) {
                ((List<WatchContext>) userdata.get(watching)).add(0, ctx);
            } else {
                if (userdata.containsKey(beforeKey) == false) {
                    userdata.put(beforeKey, new ArrayList<>());
                    String eid = execute.eventTransactionBefore().add(this::transactionBefore);
                    userdata.put(tranbeforeEIDKey, eid);
                    if (execute.inTransaction() == false) {
                        userdata.put(trankey, execute.transaction());
                    }
                }
                ((List<WatchContext>) userdata.get(beforeKey)).add(0, ctx);
            }
        }
    }

    private void executeAfter(Object sender, DbExecuteAfterArgs args) {
        DbExecute execute = (DbExecute) sender;
        Map<String, Object> userdata = execute.userdata();
        if (setting.exists(false, args.getTable(), convert(args.getOperation()))) {
            WatchContext ctx = new WatchContext();
            ctx.model = args.getModel();
            ctx.operation = convert(args.getOperation());
            ctx.table = args.getTable();
            ctx.isBefore = false;
            if (execute.inTransaction()) {
                if (userdata.containsKey(afterKey) == false) {
                    userdata.put(afterKey, new ArrayList<WatchContext>());
                    String eid = execute.eventTransactionAfter().add(this::transactionAfter);
                    userdata.put(tranafterEIDKey, eid);
                }
                ((List<WatchContext>) userdata.get(afterKey)).add(ctx);
            } else {
                addToAfterQueue(execute, ctx);
            }
        }
        if (userdata.containsKey(trankey) && userdata.containsKey(watching) == false) {
            ((Transaction) userdata.get(trankey)).commit();
            userdata.remove(trankey);
            Assert.isFalse(execute.inTransaction(), "事务流程异常");
        }
    }

    private void transactionBefore(Object sender, TransFinishArgs args) {
        DbExecute execute = (DbExecute) sender;
        Map<String, Object> userdata = execute.userdata();
        if (userdata.containsKey(beforeKey) == false) {
            return;
        }
        if (args.isCommit()) {
            try (TypeResolver tr = rootResolver.sub(p -> {
                p.bean = execute;
                p.beanType = DbExecute.class;
            })) {
                List<WatchContext> defctx = new ArrayList<>();
                userdata.put(watching, defctx);
                while (userdata.containsKey(beforeKey)) {
                    List<WatchContext> newctx = new ArrayList<>();
                    List<WatchContext> contexts = (List<WatchContext>) userdata.put(beforeKey, newctx);
                    if (contexts == null || contexts.size() < 1) {
                        break;
                    }
                    for (WatchContext context : contexts) {
                        context.resolver = tr;
                        List<WatchSetting.Def> defs = setting.gets(context.isBefore, context.table, context.operation);
                        for (WatchSetting.Def def : defs) {
                            execute(context, def);
                            defctx.forEach(ctx -> {
                                String path = String.format("%s.%s会导致循环触发。触发路径：[%s.%s]",
                                        def.method.getDeclaringClass().getCanonicalName(),
                                        def.method.getName(),
                                        ctx.table.getSimpleName(),
                                        ctx.operation.name());
                                WatchContext parent = context;
                                while (parent != null) {
                                    if (path.endsWith("]")) {
                                        path += " from ";
                                    }
                                    path += "[" + parent.table.getSimpleName() + "." + parent.operation.name() + "]";
                                    if (parent.hashCode() == ctx.hashCode()) {
                                        throw new RuntimeException(path);
                                    }
                                    parent = parent.parent;
                                }
                                ctx.parent = context;
                            });
                            newctx.addAll(defctx);
                            defctx.clear();
                        }
                    }
                }
                userdata.remove(watching);
            }
        }
        execute.eventTransactionBefore().remove((String) userdata.get(tranbeforeEIDKey));
        userdata.remove(tranbeforeEIDKey);
        userdata.remove(beforeKey);
    }

    private void transactionAfter(Object sender, TransFinishArgs args) {
        DbExecute execute = (DbExecute) sender;
        Map<String, Object> userdata = execute.userdata();
        if (args.isCommit() && userdata.containsKey(afterKey)) {
            List<WatchContext> contexts = (List<WatchContext>) userdata.get(afterKey);
            contexts.forEach(ctx -> addToAfterQueue(execute, ctx));
            userdata.remove(afterKey);
        }
        execute.eventTransactionAfter().remove((String) userdata.get(tranafterEIDKey));
        userdata.remove(tranafterEIDKey);
    }

    private void addToAfterQueue(DbExecute execute, WatchContext context) {
        afterContexts.add(context);
    }

    private void execute(WatchContext context, WatchSetting.Def def) {
        try {
            Object service = context.getService(def.serviceType);
            Object[] args = createMethodArgs(context, def);
            def.method.invoke(service, args);
        } catch (Exception e) {
            throw new RuntimeException(e.getCause() != null ? e.getCause() : e);
        }
    }

    private Object[] createMethodArgs(WatchContext context, WatchSetting.Def detail) {
        if (detail.method.getParameterCount() < 1) {
            return new Object[0];
        }
        if (detail.operation == WatchOperation.delete || detail.operation == WatchOperation.insert) {
            if (detail.table != void.class) {
                return new Object[]{context.model};
            }
            return new Object[]{context.table, context.model};
        } else if (detail.operation == WatchOperation.setNull || detail.operation == WatchOperation.update) {
            DbExecuteUpdateArgs upArgs = (DbExecuteUpdateArgs) context.model;
            if (detail.table != void.class) {
                return new Object[]{upArgs.getSet(), upArgs.getWhere()};
            }
            return new Object[]{context.table, upArgs.getSet(), upArgs.getWhere()};
        } else {
            throw new RuntimeException("不支持的监控类型：" + detail.operation.name());
        }
    }

    private WatchOperation convert(DbExecuteOperation operation) {
        switch (operation) {
            case delete:
                return WatchOperation.delete;
            case update:
                return WatchOperation.update;
            case setNull:
                return WatchOperation.setNull;
            case insert:
                return WatchOperation.insert;
            default:
                throw new RuntimeException("不支持的监听类型：" + operation.name());
        }
    }

    private void executeAfterContext() {
        Task.run(() -> {
            int delay = 100;
            while (closed != true) {
                int count = afterContexts.size();
                Task.foreach(afterContexts, ctx -> {
                    try (TypeResolver resolver = rootResolver.sub()) {
                        ctx.resolver = resolver;
                        List<WatchSetting.Def> defs = setting.gets(ctx.isBefore, ctx.table, ctx.operation);
                        for (WatchSetting.Def def : defs) {
                            execute(ctx, def);
                        }
                    }
                    afterContexts.remove(ctx);
                });
                if (count < 1) {
                    if (delay < afterDelay) {
                        delay *= 2;
                    }
                    Task.delay(delay);
                } else {
                    delay = 100;
                }
            }
            afterContexts.clear();
            afterContexts = null;
        }).onCompleted(t -> executeAfterContext());
    }

    private static class WatchContext {
        Boolean isBefore;
        Class table;
        Object model;
        WatchOperation operation;
        TypeResolver resolver;
        WatchContext parent;
        private Map<Class, Object> serviceMap = new HashMap<>();

        public Object getService(Class serviceType) {
            if (serviceMap.containsKey(serviceType) == false) {
                serviceMap.put(serviceType, resolver.resolve(serviceType));
            }
            return serviceMap.get(serviceType);
        }

        @Override
        public int hashCode() {
            int code = table.hashCode();
            code = 31 * code + (isBefore ? 0 : 1);
            code = 31 * code + operation.hashCode();
            return code;
        }
    }
}
